---
title: metro.config.js
description: A reference of available configurations in Metro.
---

import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon';

import { BoxLink } from '~/ui/components/BoxLink';
import { FileTree } from '~/ui/components/FileTree';
import { Terminal } from '~/ui/components/Snippet';

See more information about **metro.config.js** in the [customizing Metro guide](/guides/customizing-metro/).

## Environment variables

Expo CLI can load environment variables from **.env** files. Learn more about how to use environment variables in Expo CLI in the [environment variables guide](/guides/environment-variables/).

EAS CLI uses a different mechanism for environment variables, except when it invokes Expo CLI for compiling and bundling. Learn more about [environment variables in EAS](/build-reference/variables/).

If you are migrating an older project, then you should ignore local env files by adding the following to your **.gitignore**:

```sh .gitignore
# local env files
.env*.local
```

### Disabling dotenv files

Dotenv file loading can be fully disabled in Expo CLI by enabling the `EXPO_NO_DOTENV` environment variable, before invoking any Expo CLI command.

<Terminal
  cmd={[
    '# All users can run cross-env, followed by the Expo CLI command',
    '$ npx cross-env EXPO_NO_DOTENV=1 expo start',
    '# Alternatively, macOS and Linux users can define the environment variable, then run npx, followed by the Expo CLI command',
    '$ EXPO_NO_DOTENV=1 npx expo start',
  ]}
/>

### Disabling `EXPO_PUBLIC_`-prefixed client environment variables

Environment variables prefixed with `EXPO_PUBLIC_` will be exposed to the app at build-time. For example, `EXPO_PUBLIC_API_KEY` will be available as `process.env.EXPO_PUBLIC_API_KEY`.

Client environment variable inlining can be disabled with the environment variable `EXPO_NO_CLIENT_ENV_VARS=1`, this must be defined before any bundling is performed.

<Terminal
  cmd={[
    '# All users can run cross-env, followed by the Expo CLI command',
    '$ npx cross-env EXPO_NO_CLIENT_ENV_VARS=1 expo start',
    '# Alternatively, macOS and Linux users can define the environment variable, then run npx, followed by the Expo CLI command',
    '$ EXPO_NO_CLIENT_ENV_VARS=1 npx expo start',
  ]}
/>

## CSS

> **info** CSS support is under development and currently only works on web.

Expo supports CSS in your project. You can import CSS files from any component. CSS Modules are also supported.

CSS support is enabled by default. You can disable the feature by setting `isCSSEnabled` in the Metro config.

```js metro.config.js
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname, {
  // Disable CSS support.
  isCSSEnabled: false,
});
```

### Global CSS

> **warning** Global styles are web-only, usage will cause your application to diverge visually on native.

You can import a CSS file from any component. The CSS will be applied to the entire page.

Here, we'll define a global style for the class name `.container`:

```css styles.css
.container {
  background-color: red;
}
```

We can then use the class name in our component by importing the stylesheet and using `.container`:

```jsx App.js|collapseHeight=470
import './styles.css';
import { View } from 'react-native';

export default function App() {
  return (
    <>
      {/* Use `className` to assign the style with React DOM components. */}
      <div className="container">Hello World</div>

      {/* Use `style` with the following syntax to append class names in React Native for web. */}
      <View
        style={{
          $$css: true,
          _: 'container',
        }}>
        Hello World
      </View>
    </>
  );
}
```

You can also import stylesheets that are vendored in libraries, just like you would any node module:

```js index.js
// Applies the styles app-wide.
import 'emoji-mart/css/emoji-mart.css';
```

- On native, all global stylesheets are automatically ignored.
- Hot reloading is supported for global stylesheets, simply save the file and the changes will be applied.

### CSS Modules

> **warning** CSS Modules for native are under development and currently only work on web.

CSS Modules are a way to scope CSS to a specific component. This is useful for avoiding naming collisions and for ensuring that styles are only applied to the intended component.

In Expo, CSS Modules are defined by creating a file with the `.module.css` extension. The file can be imported from any component. The exported value is an object with the class names as keys and the web-only scoped names as the values. The import `unstable_styles` can be used to access `react-native-web`-safe styles.

CSS Modules support platform extensions to allow you to define different styles for different platforms. For example, you can define a `module.ios.css` and `module.android.css` file to define styles for Android and iOS respectively. You'll need to import without the extension, for example:

```diff App.js
// Importing `./App.module.ios.css`:
- import styles from './App.module.css';
+ import styles from './App.module';
```

Flipping the extension, for example, `App.ios.module.css` will not work and result in a universal module named `App.ios.module`.

> You cannot pass styles to the `className` prop of a React Native or React Native for web component. Instead, you must use the `style` prop.

```jsx App.js|collapseHeight=470
import styles, { unstable_styles } from './App.module.css';

export default function Page() {
  return (
    <>
      <Text
        style={{
          // This is how react-native-web class names are applied
          $$css: true,
          _: styles.text,
        }}>
        Hello World
      </Text>
      <Text style={unstable_styles.text}>Hello World</Text>
      {/* Web-only usage: */}
      <p className={styles.text}>Hello World</p>
    </>
  );
}
```

```css App.module.css
.text {
  color: red;
}
```

- On web, all CSS values are available. CSS is not processed or auto-prefixed like it is with the React Native Web `StyleSheet` API. You can use `postcss.config.js` to autoprefix your CSS.
- CSS Modules use [lightningcss](https://github.com/parcel-bundler/lightningcss) under the hood, check [the issues](https://github.com/parcel-bundler/lightningcss/issues) for unsupported features.

### PostCSS

[PostCSS](https://github.com/postcss/postcss) can be customized by adding a `postcss.config.json` file to the root of your project. This file should export a function that returns a PostCSS configuration object. For example:

```json postcss.config.json
{
  "plugins": {
    "tailwindcss": {}
  }
}
```

Both `postcss.config.json` and `postcss.config.js` are supported, but `postcss.config.json` enables better caching.

Expo CLI automatically handles CSS vendor prefixes with built-in support for [browserslist](https://browsersl.ist/). Avoid adding `autoprefixer` as this duplicates the functionality and slows down bundling.

#### Resetting cache after updates

Changing the Post CSS or `browserslist` config will require you to clear the Metro cache:

<Terminal
  cmd={['$ npx expo start --clear', '$ npx expo export --clear']}
  cmdCopy="npx expo start --clear && npx expo export --clear"
/>

### browserslist

Expo has automatic [browserslist](https://browsersl.ist/) support via the Rust-based CSS parser. You can customize the CSS vendor prefixes and browser support by adding a **browserslist** field to your **package.json** file. For example:

```json package.json
{
  "browserslist": [">0.2%", "not dead", "not op_mini all"]
}
```

### SASS

Expo Metro has _partial_ support for SCSS/SASS.

To setup, install the `sass` package in your project:

<Terminal cmd={['$ yarn add -D sass']} />

Then, ensure [CSS is setup](#css) in the **metro.config.js** file.

- When `sass` is installed, then modules without extensions will be resolved in the following order: `scss`, `sass`, `css`.
- Only use the intended syntax with `sass` files.
- Importing other files from inside a scss/sass file is not currently supported.

### Tailwind

> **info** Standard Tailwind CSS supports only web platform. For universal support, use a library such as [NativeWind](https://www.nativewind.dev/), which allows creating styled React Native components with Tailwind CSS.

<BoxLink
  title="Tailwind CSS"
  description="Learn how to configure and use Tailwind CSS in your Expo project."
  href="/guides/tailwind/"
  Icon={BookOpen02Icon}
/>

## Extending the Babel transformer

Expo's Metro config uses a custom `transformer.babelTransformerPath` value to ensure `expo-babel-preset` is always used and web/Node.js environments are supported.

If you want to extend the Babel transformer, import the upstream transformer from `@expo/metro-config/babel-transformer` instead of `metro-react-native-babel-transformer`. For example:

```js metro.transformer.js
const upstreamTransformer = require('@expo/metro-config/babel-transformer');

module.exports.transform = async ({ src, filename, options }) => {
  // Do something custom for SVG files...
  if (filename.endsWith('.svg')) {
    src = '...';
  }
  // Pass the source through the upstream Expo transformer.
  return upstreamTransformer.transform({ src, filename, options });
};
```

## Custom resolving

Expo CLI extends the default Metro resolver to add features like Web, Server, and tsconfig aliases support. You can similarly customize the default resolution behavior of Metro by chaining the `config.resolver.resolveRequest` function.

```tsx metro.config.js|collapseHeight=470
const { getDefaultConfig } = require('expo/metro-config');

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

config.resolver.resolveRequest = (context, moduleName, platform) => {
  if (moduleName.startsWith('my-custom-resolver:')) {
    // Logic to resolve the module name to a file path...
    // NOTE: Throw an error if there is no resolution.
    return {
      filePath: 'path/to/file',
      type: 'sourceFile',
    };
  }

  // Ensure you call the default resolver.
  return context.resolveRequest(context, moduleName, platform);
};

module.exports = config;
```

Unlike traditional bundlers, Metro shared the same resolver function across all platforms. As a result, you can mutate the resolution settings dynamically on each request with the `context` object.

### Mocking modules

If you want a module to be empty for a given platform, you can return a `type: 'empty'` object from the resolver. The following example will cause `lodash` to be empty on web:

```ts metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

config.resolver.resolveRequest = (context, moduleName, platform) => {
  if (platform === 'web' && moduleName === 'lodash') {
    return {
      type: 'empty',
    };
  }

  // Ensure you call the default resolver.
  return context.resolveRequest(context, moduleName, platform);
};

module.exports = config;
```

This technique is equivalent to using empty externals in Webpack or Vite, but with the added benefit of being able to target specific platforms.

### Virtual modules

Metro doesn't support virtual modules at the moment. One technique you can use to obtain similar behavior is to create a module in the `node_modules/.cache/...` directory and redirect the resolution to that file.

The following example will create a module at `node_modules/.cache/virtual/virtual-module.js` and redirect the resolution of `virtual:my-module` to that file:

```ts metro.config.js
const path = require('path');
const fs = require('fs');

const { getDefaultConfig } = require('expo/metro-config');

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

const virtualPath = path.resolve(__dirname, 'node_modules/.cache/virtual/virtual-module.js');

// Create the virtual module in a generated directory...
fs.mkdirSync(path.dirname(virtualPath), { recursive: true });
fs.writeFileSync(virtualPath, 'export default "Hello World";');

config.resolver.resolveRequest = (context, moduleName, platform) => {
  if (moduleName === 'virtual:my-module') {
    return {
      filePath: virtualPath,
      type: 'sourceFile',
    };
  }

  // Ensure you call the default resolver.
  return context.resolveRequest(context, moduleName, platform);
};

module.exports = config;
```

This can be used to emulate `externals` with custom imports. For example, if you want to redirect `require('expo')` to something custom like `SystemJS.require('expo')`, you can create a virtual module that exports `SystemJS.require('expo')` and redirect the resolution of `expo` to that file.

## Custom transforming

> Transformations are heavily cached in Metro. If you update something, use the `--clear` flag to see updates. For example, `npx expo start --clear`.

Metro doesn't have a very expressive plugin system for transforming files, instead opt to use the [**babel.config.js**](../config/babel/) and caller object to customize the transformation.

```js babel.config.js
module.exports = function (api) {
  // Get the platform that Expo CLI is transforming for.
  const platform = api.caller(caller => (caller ? caller.platform : 'ios'));

  // Detect if the bundling operation is for Hermes engine or not, e.g. `'hermes'` | `undefined`.
  const engine = api.caller(caller => (caller ? caller.engine : null));

  // Is bundling for a server environment, e.g. API Routes.
  const isServer = api.caller(caller => (caller ? caller.isServer : false));

  // Is bundling for development or production.
  const isDev = api.caller(caller =>
    caller
      ? caller.isDev
      : process.env.BABEL_ENV === 'development' || process.env.NODE_ENV === 'development'
  );

  // Ensure the config is not cached otherwise the platform will not be updated.
  api.cache(false);
  // You can alternatively provide a more robust CONFIG cache invalidation:
  // api.cache.invalidate(() => platform);

  return {
    presets: ['babel-preset-expo'],
    plugins: [
      // Add a plugin based on the platform...
      platform === 'web' && 'my-plugin',

      // Ensure you filter out falsy values.
    ].filter(Boolean),
  };
};
```

If the caller doesn't have `engine`, `platform`, `bundler`, and so on, then ensure you are using `@expo/metro-config/babel-transformer` for the transformer. If you're using a custom transformer then it may need to extend the Expo transformer.

Always try to implement custom logic in the resolver if possible, caching is much simpler and easier to reason about. For example, if you need to remap an import, it's simpler and faster to resolve to a static file with the resolver than to parse all possible import methods and remap them with the transformer.

Always use `babel-preset-expo` as the default Babel preset, this ensures the transformation is always compatible with Expo runtimes. `babel-preset-expo` uses all of the caller inputs internally to optimize for a given platform, engine, and environment.

## Node.js built-ins

When bundling for a server environment, Expo's Metro config automatically supports externalizing Node.js built-in modules (`fs`, `path`, `node:crypto`, and more) based on the current Node.js version. If the CLI is bundling for a browser environment, then built-ins will first check if the module is installed locally, then fallback on an empty shim. For example, if you install `path` for use in the browser, this can be used, otherwise, the module will automatically be skipped.

## Environment settings

> **info** These environment variables will not be defined in test environments.

Expo's Metro config injects build settings that can be used in the client bundle via environment variables. All variables will be inlined and cannot be used dynamically. For example, `process.env["EXPO_BASE_URL"]` won't work.

- `process.env.EXPO_BASE_URL` exposes the base URL defined in `experiments.baseUrl`. This is used in Expo Router to respect the production base URL for deployment.

## Bundle splitting

Expo CLI automatically splits web bundles into multiple chunks based on async imports in production. This feature requires `@expo/metro-runtime` to be installed and imported somewhere in the entry bundle (available by default in Expo Router).

Shared dependencies of async bundles are merged into a single chunk to reduce the number of requests. For example, if you have two async bundles that import `lodash`, then the library is merged into a single initial chunk.

The chunk splitting heuristic cannot be customized. For example:

<FileTree files={['math.js', 'index.js']} />

```js math.js
export function add(a, b) {
  return a + b;
}
```

```js index.js
import '@expo/metro-runtime';

// This will be split into a separate chunk.
import('./math').then(math => {
  console.log(math.add(1, 2));
});
```

When you run `npx expo export -p web`, the bundles are split into multiple files, and the entry bundle is added to the main HTML file. `@expo/metro-runtime` adds the runtime code that loads and evaluates the async bundles.

## Source map debug ID

If a bundle is exported with an external source map, a [**Debug ID**](https://sentry.engineering/blog/the-case-for-debug-ids) annotation will be added to the end of the file, along with a matching `debugId` in the source map for corresponding the files together. If no source maps are exported, or inline source maps are used then this annotation will not be added.

```js
// <all source code>

//# debugId=<deterministic chunk hash>
```

The associated `*.js.map` or `*.hbc.map` source map will be a JSON file containing an equivalent `debugId` property. The `debugId` will be injected before hermes bytecode generation to ensure matching in all cases.

The `debugId` is a deterministic hash of the bundle's contents without the external bundle splitting references. This is the same value used to create a chunks filename but formatted as a UUID. For example, `431b98e2-c997-4975-a3d9-2987710abd44`.

`@expo/metro-config` injects `debugId` during `npx expo export` and `npx expo export:embed`. Any additional optimization steps in `npx expo export:embed` like Hermes bytecode generation will need to have the `debugId` injected manually.

## Metro require runtime

You can optionally enable a custom Metro `require` implementation with the environment variable `EXPO_USE_METRO_REQUIRE=1`. This runtime has the following features:

- String module IDs that are human-readable and make missing module errors easier to follow.
- Deterministic IDs that are the same between runs and across modules (required for React Server Components in development).
- Removed support for legacy RAM bundles.

## Magic import comments

> Available from SDK 52 on all platforms.

Server environments such as Workers, and Node.js support import arbitrary files at runtime, so you may want to keep `import` syntax in-tact instead of using Metro's require system. You can opt-out dynamic imports with the `/* @metro-ignore */` comment in `import()` statements.

```js
// Manually ensure `./my-module.js` is included in the correct spot relative to the module.
const myModule = await import(/* @metro-ignore */ './my-module.js');
```

Expo CLI will skip the `./my-module.js` dependency and assume that the developer has manually added it to the output bundle. Internally, this is used for exporting custom server code that dynamically switches between files based on the request. Avoid using this syntax for native bundles since `import()` is generally not available in React Native with Hermes enabled.

Many React libraries shipped the Webpack `/* webpackIgnore: true */` comment to achieve similar behavior. To bridge the gap, we've also added support for Webpack's comment but recommend using the Metro equivalent in your app.

## ES Module resolution

> This sections applies from SDK 53 on all platforms.

Metro resolves ES Module `import` and CommonJS `require` with separate resolution strategies.

Previously, Metro applied the classic Node.js module resolution strategy (which matches Node.js versions before v12), with some additions to support ES Modules. In this resolution strategy, Metro resolves modules from `node_modules`, JS files, optionally while omitting extensions, such as `.js`, and uses `package.json` fields such as `main`, `module`, and `react-native`.

Now, with the modern ES Modules resolution strategy, Metro instead resolves modules from `node_modules`, then matches different `package.json` fields, such as `exports`, [a nested map of sub-paths a package exposes](https://nodejs.org/api/packages.html#conditional-exports), and `main`.

Depending on how a package is imported, one of these two resolution strategies will be used. Typically, a file that is imported with `import` from a Node module (rather than `require`), will use the ES Modules resolution strategy, and fall back on regular classic Node.js resolution. A file that wasn't resolved with ES Modules resolution or has been imported with CommonJS `require` will use the classic resolution strategy.

### `package.json:exports`

When performing ES Modules resolution, Metro will look at the `package.json:exports` conditions map. This is a mapping of import subpaths and conditions to files in the Node module package.

For example, a package that always exposes an **index.js** file, and matches Metro's classic CommonJS module resolution, may specify a map with the `default` condition.

```json
{
  "exports": {
    "default": "./index.js"
  }
}
```

However, a package providing both a CommonJS and ES Modules entrypoint may provide a mapping with the `import` and `require` conditions.

```json
{
  "exports": {
    "import": "./index.mjs",
    "require": "./index.cjs"
  }
}
```

By default, Metro will match different conditions depending on the platform and whether the resolution has started from a CommonJS `require` call, or an ES Modules `import` statement and will change the condition accordingly.

For native platforms, the condition `react-native` is added, for web exports, the `browser` condition is added, and for server exports (such as API routes or React Server functions), the `node`, `react-server`, and `workerd` conditions are added. These conditions aren't matched in the order they're defined in. Instead, they're matched against the order of properties in the `package.json:exports` map.

TypeScript performs ES Module resolution separately from Metro and will also respect `package.json:exports` maps, when its `compilerOptions.moduleResolution` configuration option has either been set to `"bundler"` (which matches Metro's behavior more closely) or to `"node16"` / `"nodenext"`. TypeScript will however also match the `types` condition. As such, types may not resolve properly when a package doesn't put the `types` condition first in its exports map.

Since an exports map may contain subpaths, a package import may not have to match a file in the package's modules folder any longer, but may be a "redirected" import. Importing `'package/submodule'` may match a different file than **node_modules/package/submodule.js** if it's specified in `package.json:exports`.

```json
{
  "exports": {
    ".": "./index.js",
    "./submodule": "./submodule/submodule.js"
  }
}
```

If you're encountering packages that are incompatible or unprepared for the new ES Modules resolution strategy, you may be able to resolve problems by patching its `package.json` file and add or correct its `package.json:exports` conditions map. However, it's also possible to prevent Metro from using `package.json:exports` maps in its resolution by disabling the `unstable_enablePackageExports` option.

```js metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

config.resolver.unstable_enablePackageExports = false;

module.exports = config;
```

## Asset imports

When assets are imported, a virtual module is created to represent the data required for importing the asset.

On native platforms, an asset will be a numeric ID: `1`, `2`, `3`, and so on, which can be looked up using `require("@react-native/assets-registry/registry").getAssetByID(<NUMBER>)`. On web and server platforms, the asset will change depending on the file type. If the file is an image, then the asset will be `{ uri: string, width?: number, height?: number }`, otherwise the asset will be a `string` representing the remote URL for the asset.

The assets can be used as follows:

```jsx
import { Image } from 'react-native';

import asset from './img.png';

function Demo() {
  return <Image source={asset} />;
}
```

In API routes, you can always assume the type of the asset will not be a number:

```js
import asset from './img.png';

export async function GET(req: Request) {
  const ImageData = await fetch(
    new URL(
      // Access the asset URI.
      asset.uri,
      // Append to the current request URL origin.
      req.url
    )
  ).then(res => res.arrayBuffer());

  return new Response(ImageData, {
    headers: {
      'Content-Type': 'image/png',
    },
  });
}
```

## Web workers

> **important** This feature is experimental and subject to breaking changes.

```ts
new Worker(new URL('./worker', window.location.href));
```

Expo Metro has experimental web worker support. This feature is currently web-only and does not work on native, usage on native will trigger an error "Property 'Worker' doesn't exist".

Web workers can be used to offload work to a separate thread on web, allowing the main thread to remain responsive. This is useful for computationally expensive tasks, such as image processing, cryptography, or other tasks that would otherwise block the main thread.

Workers can be generated inline using `Blob`, but sometimes you may want to leverage modern features like TypeScript or importing other modules.

Web workers depend on Expo bundle splitting support, which means you need to either use Expo Router or install and import `@expo/metro-runtime`. You also cannot use the environment `EXPO_NO_METRO_LAZY=1` with web workers.

Consider the following example of a worker that doubles a number:

```ts worker.ts
self.onmessage = ({ data }) => {
  const result = data * 2; // Example: double the number
  self.postMessage(result);
};
```

This worker file can be imported as a `Worker` in the main app:

```ts
// worker is of type `Worker`
const worker = new Worker(new URL('./worker', window.location.href));

worker.onmessage = ({ data }) => {
  console.log(`Worker responded: ${data}`);
};

worker.postMessage(5);
```

Behind the scenes, Expo CLI is generating code like this:

```ts
const worker = new Worker(
  new URL('/worker.bundle?platform=web&dev=true&etc', window.location.href)
);
```

The generated bundle URL changes based on development/production to ensure the worker is loaded and bundled correctly. Unlike traditional bundle splitting, a worker file needs to contain its own copy of all modules and cannot depend on common modules in the main bundle.

The native API `Worker` is traditionally unavailable in React Native and not provided by the Expo SDK, so even though this bundling feature technically works for all platforms, it's only useful on web. You could theoretically write a native Expo module that polyfills the `Worker` API if you want to support native platforms too. Alternatively, you can use the "worklet" API in React Native Reanimated to offload work to a separate thread on native.

Alternatively, you can import Workers using the public path by first putting a transformed JS file in the **public** directory, then referencing it in the worker import with a variable:

```ts
// Will avoid the transform and use the public path directly.
const worker = new Worker('/worker.js');

// The variable breaks the transform causing the literal path to be used instead of the transformed path.
const path = '/worker.js';

const anotherWorker = new Worker(new URL(path, window.location.href));
```

Using a variable in the `Worker` constructor is not supported for bundling. To inspect the internal URL, you may use the internal syntax `require.unstable_resolveWorker('./path/to/worker.js')` to get the URL fragment.

## Bare workflow setup

> This guide is versioned and will need to be revisited when upgrading/downgrading Expo. Alternatively, use [Expo Prebuild](/workflow/prebuild) for fully automated setup.

Projects that don't use [Expo Prebuild](/workflow/prebuild) must configure native files to ensure the Expo Metro config is always used to bundle the project.

{/* If this isn't done, then features like [aliases](/guides/typescript/#path-aliases-optional), [absolute imports](/guides/typescript/#absolute-imports-optional), asset hashing, and more will not work. */}

These modifications are meant to replace `npx react-native bundle` and `npx react-native start` with `npx expo export:embed` and `npx expo start` respectively.

### metro.config.js

Ensure the **metro.config.js** extends `expo/metro-config`:

```js metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

module.exports = config;
```

### `android/app/build.gradle`

The Android **app/build.gradle** must be configured to use Expo CLI for production bundling. Modify the `react` config object:

```diff android/app/build.gradle
react {
  ...
+     // Use Expo CLI to bundle the app, this ensures the Metro config
+     // works correctly with Expo projects.
+     cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim())
+     bundleCommand = "export:embed"
}
```

### `ios/<Project>.xcodeproj/project.pbxproj`

In your **ios/&lt;Project&gt;.xcodeproj/project.pbxproj** file, replace the following scripts:

#### "Start Packager" script

Remove the **"Start Packager"** script. The dev server must be started with `npx expo` before/after running the app.

```diff Start Packager
-    FD10A7F022414F080027D42C /* Start Packager */ = {
-			isa = PBXShellScriptBuildPhase;
-			alwaysOutOfDate = 1;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputFileListPaths = (
-			);
-			inputPaths = (
-			);
-			name = "Start Packager";
-			outputFileListPaths = (
-			);
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\nexport RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > `$NODE_BINARY --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'\"`\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n  if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n    if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n      echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n      exit 2\n    fi\n  else\n    open `$NODE_BINARY --print \"require('path').dirname(require.resolve('expo/package.json')) + '/scripts/launchPackager.command'\"` || echo \"Can't start packager automatically\"\n  fi\nfi\n";
-			showEnvVarsInLog = 0;
-		};
```

#### "Bundle React Native code and images" script

```diff Bundle React Native code and images
+			shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n  export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n  # Set the entry JS file using the bundler's entry resolution.\n  export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n  # Use Expo CLI\n  export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n  # Default Expo CLI command for bundling\n  export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
```

Alternatively, in the Xcode project, select the **"Bundle React Native code and images"** build phase and add the following modifications:

```diff Bundle React Native code and images
if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then
  source "$PODS_ROOT/../.xcode.env"
fi
if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
  source "$PODS_ROOT/../.xcode.env.local"
fi

# The project root by default is one level up from the ios directory
export PROJECT_ROOT="$PROJECT_DIR"/..

if [[ "$CONFIGURATION" = *Debug* ]]; then
  export SKIP_BUNDLING=1
fi
+ if [[ -z "$ENTRY_FILE" ]]; then
+   # Set the entry JS file using the bundler's entry resolution.
+   export ENTRY_FILE="$("$NODE_BINARY" -e "require('expo/scripts/resolveAppEntry')" "$PROJECT_ROOT" ios absolute | tail -n 1)"
+ fi

+ if [[ -z "$CLI_PATH" ]]; then
+   # Use Expo CLI
+   export CLI_PATH="$("$NODE_BINARY" --print "require.resolve('@expo/cli')")"
+ fi
+ if [[ -z "$BUNDLE_COMMAND" ]]; then
+   # Default Expo CLI command for bundling
+   export BUNDLE_COMMAND="export:embed"
+ fi

`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"`
```

> You can set `CLI_PATH`, `BUNDLE_COMMAND`, and `ENTRY_FILE` environment variables to overwrite these defaults.

### Custom entry file

By default, React Native only supports using a root `index.js` file as the entry file (or platform-specific variation like `index.ios.js`). Expo projects allow using any entry file, but this requires addition bare setup.

#### Development

Development mode entry files can be enabled by using the [`expo-dev-client`](../sdk/dev-client/) package. Alternatively you can add the following configuration:

```diff ios/<Project>/AppDelegate.mm
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
-  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
+  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
```

```diff android/app/src/main/java/<Project>/MainApplication.java
@Override
protected String getJSMainModuleName() {
-  return "index";
+  return ".expo/.virtual-metro-entry";
}
```

#### Production

In your **ios/&lt;Project&gt;.xcodeproj/project.pbxproj** file, replace the **"Bundle React Native code and images"** script to set `$ENTRY_FILE` according using Metro:

```diff ios/<Project>/project.pbxproj
+			shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n  source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n  export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n  # Set the entry JS file using the bundler's entry resolution.\n  export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n  # Use Expo CLI\n  export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n  # Default Expo CLI command for bundling\n  export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
```

The Android **app/build.gradle** must be configured to use Metro module resolution to find the root entry file. Modify the `react` config object:

```diff app/build.gradle
+ def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()

react {
+    entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
}
```
